Grouping List Items in ASP.Net DropDownList

As many of us know,currently ASP.Net dropdownlist control doesn’t support grouping list items,though you can create it easily in pure html using "select" :

<select id="myselect">
    <optgroup label="COUNTRY">
        <option label="country1">country1</option>
        <option label="country2">country2</option>
    </optgroup>
    <optgroup label="STATE">
        <option label="STATE1">STATE1</option>
        <option label="STATE2">STATE2</option>
    </optgroup>
</select>

Which will be rendered as:

as you can see it's easily done using “select” tag along with optgroup,you can refer to W3C site for more information on this tag.

In order to add the grouping support to ASP.Net dropdownlist we have to customize it by coding

How to do it in ASP.NET DropDownList:

Asp.net dropdownlist doesn't support grouping by default,we have to override it’s functionality/rending. Here are the steps:

    1. Create a new project of type ClassLibrary and name it “GroupDropDownList
    2. Remove any class files added by the project then create a new class file named “GroupDropDownList.cs
    3. Add reference to “System.Web
    4. Replace GroupDropDownList.cs code with this:
using System;
using System.ComponentModel;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Collections;

namespace GroupDropDownList
{
    /// <summary>
    /// Summary description for GroupDropDownList.
    /// </summary>
    [ToolboxData("<{0}:GroupDropDownList runat=server></{0}:GroupDropDownList>")]
    public class GroupDropDownList : DropDownList
    {
        /// <summary>
        /// The field in the datasource which provides values for groups
        /// </summary>
        [DefaultValue(""), Category("Data")]
        public virtual string DataGroupField
        {
            get
            {
                object obj = ViewState["DataGroupField"];
                if (obj != null)
                {
                    return (string)obj;
                }
                return string.Empty;
            }
            set
            {
                ViewState["DataGroupField"] = value;
            }
        }
        /// <summary>
        /// if a group doesn't has any enabled items,there is no need
        /// to render the group too
        /// </summary>
        /// <param name="groupName"></param>
        /// <returns></returns>
        private bool IsGroupHasEnabledItems(string groupName)
        {
            ListItemCollection items = Items;
            for (int i = 0; i < items.Count; i++)
            {
                ListItem item = items[i];
                if (item.Attributes["DataGroupField"].Equals(groupName) && item.Enabled)
                {
                    return true;
                }
            }
            return false;
        }
        /// <summary>
        /// Render this control to the output parameter specified.
        /// Based on the source code of the original DropDownList method
        /// </summary>
        /// <param name="writer"> The HTML writer to write out to </param>
        protected override void RenderContents(HtmlTextWriter writer)
        {
            ListItemCollection items = Items;
            int itemCount = Items.Count;
            string curGroup = String.Empty;
            bool bSelected = false;

            if (itemCount <= 0)
            {
                return;
            }

            for (int i = 0; i < itemCount; i++)
            {
                ListItem item = items[i];
                string itemGroup = item.Attributes["DataGroupField"];
                if (itemGroup != null && itemGroup != curGroup && IsGroupHasEnabledItems(itemGroup))
                {
                    if (curGroup != String.Empty)
                    {
                        writer.WriteEndTag("optgroup");
                        writer.WriteLine();
                    }

                    curGroup = itemGroup;
                    writer.WriteBeginTag("optgroup");
                    writer.WriteAttribute("label", curGroup, true);
                    writer.Write('>');
                    writer.WriteLine();
                }
                // we don't want to render disabled items
                if (item.Enabled)
                {
                    writer.WriteBeginTag("option");
                    if (item.Selected)
                    {
                        if (bSelected)
                        {
                            throw new HttpException("Cant_Multiselect_In_DropDownList");
                        }
                        bSelected = true;
                        writer.WriteAttribute("selected", "selected", false);
                    }

                    writer.WriteAttribute("value", item.Value, true);
                    writer.Write('>');
                    HttpUtility.HtmlEncode(item.Text, writer);
                    writer.WriteEndTag("option");
                    writer.WriteLine();
                }
            }
            if (curGroup != String.Empty)
            {
                writer.WriteEndTag("optgroup");
                writer.WriteLine();
            }
        }

        /// <summary>
        /// Perform data binding logic that is associated with the control
        /// </summary>
        /// <param name="e">An EventArgs object that contains the event data</param>
        protected override void OnDataBinding(EventArgs e)
        {
            // Call base method to bind data
            base.OnDataBinding(e);

            if (DataGroupField == String.Empty)
            {
                return;
            }
            // For each Item add the attribute "DataGroupField" with value from the datasource
            IEnumerable dataSource = GetResolvedDataSource(DataSource, DataMember);
            if (dataSource != null)
            {
                ListItemCollection items = Items;
                int i = 0;

                string groupField = DataGroupField;
                foreach (object obj in dataSource)
                {
                    string groupFieldValue = DataBinder.GetPropertyValue(obj, groupField, null);
                    ListItem item = items[i];
                    item.Attributes.Add("DataGroupField", groupFieldValue);
                    i++;
                }
            }

        }

        /// <summary>
        /// This is copy of the internal ListControl method
        /// </summary>
        /// <param name="dataSource"></param>
        /// <param name="dataMember"></param>
        /// <returns></returns>
        private IEnumerable GetResolvedDataSource(object dataSource, string dataMember)
        {
            if (dataSource != null)
            {
                var source1 = dataSource as IListSource;
                if (source1 != null)
                {
                    IList list1 = source1.GetList();
                    if (!source1.ContainsListCollection)
                    {
                        return list1;
                    }
                    var list = list1 as ITypedList;
                    if (list != null)
                    {
                        var list2 = list;
                        PropertyDescriptorCollection collection1 = list2.GetItemProperties(new PropertyDescriptor[0]);
                        if ((collection1 == null) || (collection1.Count == 0))
                        {
                            throw new HttpException("ListSource_Without_DataMembers");
                        }

                        PropertyDescriptor descriptor1 = collection1[0];

                        if (!string.IsNullOrWhiteSpace(dataMember))
                        {
                            descriptor1 = collection1.Find(dataMember, true);
                        }

                        if (descriptor1 != null)
                        {
                            object obj1 = list1[0];
                            object obj2 = descriptor1.GetValue(obj1);
                            var enumerable = obj2 as IEnumerable;
                            if (enumerable != null)
                            {
                                return enumerable;
                            }
                        }
                        throw new HttpException("ListSource_Missing_DataMember");
                    }
                }
                var source = dataSource as IEnumerable;
                if (source != null)
                {
                    return source;
                }
            }
            return null;
        }
        #region Internal behaviour
        /// <summary>
        /// Saves the state of the view.
        /// </summary>
        protected override object SaveViewState()
        {
            // Create an object array with one element for the CheckBoxList's
            // ViewState contents, and one element for each ListItem in skmCheckBoxList
            var state = new object[Items.Count + 1];

            object baseState = base.SaveViewState();
            state[0] = baseState;

            // Now, see if we even need to save the view state
            bool itemHasAttributes = false;
            for (int i = 0; i < Items.Count; i++)
            {
                if (Items[i].Attributes.Count == 0) continue;

                itemHasAttributes = true;

                // Create an array of the item's Attribute's keys and values
                var attribKv = new object[Items[i].Attributes.Count * 2];
                int k = 0;
                foreach (string key in Items[i].Attributes.Keys)
                {
                    attribKv[k++] = key;
                    attribKv[k++] = Items[i].Attributes[key];
                }

                state[i + 1] = attribKv;
            }

            // return either baseState or state, depending on if any ListItems had attributes
            return itemHasAttributes ? state : baseState;
        }

        /// <summary>
        /// Loads the state of the view.
        /// </summary>
        /// <param name="savedState">State of the saved.</param>
        protected override void LoadViewState(object savedState)
        {
            if (savedState == null) return;

            // see if savedState is an object or object array
            var objects = savedState as object[];
            if (objects != null)
            {
                // we have an array of items with attributes
                object[] state = objects;
                base.LoadViewState(state[0]); // load the base state

                for (int i = 1; i < state.Length; i++)
                {
                    if (state[i] != null)
                    {
                        // Load back in the attributes
                        var attribKv = (object[]) state[i];
                        for (int k = 0; k < attribKv.Length; k += 2)
                            Items[i - 1].Attributes.Add(attribKv[k].ToString(),
                                attribKv[k + 1].ToString());
                    }
                }
            }
            else
            {
                // we have just the base state
                base.LoadViewState(savedState);
            }
        }
        #endregion
    }
}
    1. Open your website project then add a reference to “GroupDropDownList.dll
    2. In your aspx page add a register directive:
<%@ Register TagPrefix="customControl" Namespace="GroupDropDownList" Assembly="GroupDropDownList" %>
  1. Now add your control like this:
    <customControl:GroupDropDownList ID="ddlUsers" runat="server"></customControl:GroupDropDownList>
    

How to bind the control:

let's assume our data looks like this:

Id

Username UserType
1 John Admins
2 Joey Admins
3 Majid Users
4 Sam Users

Before you bind the data to your control make sure your data is sorted by DataGroupField ,this is a must:

DataView myview = usersDatatable.DefaultView;
myview.Sort = "UserType asc";
ddlUsers.DataSource = myview;
ddlUsers.DataTextField = "Username";
ddlUsers.DataValueField = "Id";
ddlUsers.DataGroupField = "UserType";
ddlUsers.DataBind();


DataGoupField must contains the name of the column that it will be used as a group title

so the data in the groupDropDownList will show your data like this:

Pretty cool code but wait a second ?! Explain to me the code in our class library:

A) What is DataGroupField ? :

we created a property and named it "DataGroupField" (yeah,I tried to name it something similar to those DataTextField and DataValueField to make it more appropriate) and make it appears as a property in group "Data",but I needed to store it's value in a viewstate so that I will not lose it in postbacks,and it's value by default is empty string.

B) What does IsGroupHasEnabledItems do ? :

You may want to disable some items and that's fine,however if you disabled all items of a group then you don't want to render that Title of the group at all,so the main purpose of this method is to check if that group still has any enabled items or not

C) Why did we overrided RenderContents ? :

We need to override how our control is rendered,as you remember we need grouping so we need to customize how our control is rendered to achieve the same results we achieved in our sample of "select" tag.

please read my comments in that method

D:) Why did we overrided OnDataBinding ? :

This function is called on binding before calling RenderContent,but in RenderContent I will never know the current item's group which it's going to be rendered,so that's gave me an idea..I can loop within my items and check if that property (DataGroupField) is not empty then you need grouping,but I need to store an attribute on the item so I can read from it later on in RenderContents method to know to which group is that item..tricky right

E:) What does that GetResolvedDataSource do ? :

Just to check if the datasource is valid or not

F:) Why did we overrided SaveViewState and LoadViewState ? :

When the control postback or refreshed,control will be rendered once again but without binding(without calling OnDataBinding),which means that our items will be without attributes(if you remember,OnDataBinding is the one who adds the attribute DataGroupField which later on is used in RenderContents method and used to define item's group) and thus the grouping will not be rendered.

so what's the solution? we need to override how control's viewstate is saved and retrieved,WHY? because we want to store the attribute into viewstate and load it once again,that way when the control is rendered,our items will keep their attributes within it's viewstate and thus the grouping will be rendered..do you find it tricky?

FAQ:

1) I'm using DataReader,not DataTable so what should I do?

You need to fill your data from the datareader into a temporary datatable object,then use a DataView to sort your data then use it as a datasource for your control

2) What if I want to add an item within a group,can I do that?

Do it like this:

ListItem egypt = new ListItem("Egypt", "4");
egypt.Attributes.Add("DataGroupField", "Africa Countries");
GroupDropDownList1.Items.Add(egypt);

Credits:

I want to thank Scott Mitchell for his great article "ListControl Items, Attributes, and ViewState" and I give him the creadit for using his code "LoadViewState and SaveViewState".

Also I want to thank lotuspro for his ideas in his article "ASP.NET DropDownList with OptionGroup support"

P.S. In addition to blogging, I am also now using Twitter for quick updates and to share links. Follow me at: twitter.com/alaa9jo

56 Comments

  • I've been trying for hours to get this to work without success. I followed the instructions exactly and I can compile the DLL but I'm trying to add it to a usercontrol and it keeps giving the error:

    The type or namespace name 'GroupDropDownList' could not be found in the global namespace (are you missing an assembly reference?).


    The ascx page contains:



    and



    Any advice?

  • the dll is not added as a reference to your project,try to add it then let me know the results.

    I always prefer to add the groupdropdownlist control to your toolbox then drag-n-drop it to anywhere you want i.e.to the usercontrol like in your case.

  • Thnx mate.. Works like a charm....

  • Excellent!!!
    This code is perfect. It worked perfect. Thank you very much.

  • Excellent. This worked without problems. I banged my head for a day trying to get the SharpPieces optgroup component to work without luck. This was much easier. You should make an effort to publicize this more. It was on page two of Google for me.

    thanks again.

  • Ok, perhaps I spoke too soon. The control appears properly rendered, but it is not returning the expected values. I am using Data Binding and it builds the control just fine (it looks right, grouped, etc). However, it pre-selects the first item in the list (that item ends up with a selected="selected" attribute on that option). When I attempt to access the selected value using myControlID.selectedValue, I always get the value of the first item in the list, even if the user changes the selection to something else.

    If I use the myControlID.selectedValue = value line in my Page_Load, then it will pre-select a different item to start with, but then it will always return that pre-selected item's value, even if the user changes the selection.

    How can I access the value that the user has selected. I am trying to use the value in a function fired from a button onClick event. Help!

  • Ok, never mind. &nbsp;I forgot the "If Not Page.IsPostBack".

  • Excellent article..

  • thanks it works for me.
    Excellent work

  • But I am getting sapce between state Name and city Name

    please tell me how to remove space from statename and city name

  • The control should render exactly as in the example above,if there is any spaces then you have check on the css in your page and any styles you add to tags like: select,optgroup,option.

    If that didn't help then creat a test page then try the control,if it worked (and it should) then you have to add the styles/css files one by one,this will help you to diagnose the issue.

  • i created test page there also the same output (space in datagroupField)
    if u give me your email id then i will send u the scrrenshot of the output

  • Upload the image at: http://imageshack.us/

  • i uploaded image at given url
    name as outputt.png

  • i got it .
    done .
    i make some mistakes in arranging tablefields.
    thanks alot buddy.

  • I'm glad that it's working for you.

  • thank you very much. you are doing nice job

  • grouping is not working when bind dropdown using


    below is the code :-




  • after bind datasource ...how to insert item at 0th position like Please select

  • Currently I didn't handle the code to append dataitems and using datasources like objectdatasource at the same time,mmmm...maybe I will make an update on the code to handle that,maybe in the coming days.

    There is a workaround; you can append items only by code after binding:

    GroupDropDownList1.DataBind();
    ListItem selectAnItem = new ListItem("Select an item", "-1");
    selectAnItem.Attributes.Add("DataGroupField", "");
    GroupDropDownList1.Items.Insert(0, selectAnItem);

  • Great Work Dude!

    Got this up and running in 15mins! Thanks a ton!

  • Works great, thanks!

  • Great...
    looking for this
    thak u very much.

  • hi thnks for that!!!!!!!!!1

  • Very Nice!!!

    Thank You.

  • What are the licensing requirements surrounding this code? for commercial applications?

  • @Michael : This code is provided for free,you can use it in commercial applications.

  • I am looking the same thing...and this help a lot i got resolve my problem.

    Thanks Ala'a Alnajjar for your great work and explanation you have provided.

    Thanks

  • Youre completely correct on this blog..

  • It works great!! Thanks mate..

  • Cool one. thanks for a great post...

  • i need to update my sql data using the selected item from the list and it's not working

    cmd.Parameters.Add("category", SqlDbType.VarChar);
    cmd.Parameters["category"].Value = this.gddlCategory.Text;

  • Works like charm, been trying for this type of group control for months...Thanks!

  • Superb article. I was looking for this functionality and found your article via google search. Very helpful!!!

    Thanks very much and keep sharing.

  • This is wonderful--thanks. Was wondering how to have a value pre-selected? Seems like Items.FindByValue doesn't work...

    Thanks

  • Excellent work, thanks !

  • Hi,

    Thanks for posting this!

    However, I did exactly as described and I am now getting the following error:

    Index was outside the bounds of the array.

    Any ideas?

  • Thank you so much ..Great effort

  • Muchisimas gracias!!

  • Great tool nice work thank it help us lot

  • Thanks for this. Had me stumped for ages.

    Krishan August 19 2010

    There are probably more elegant ways to do it but I replaced:

    IEnumerable dataSource = GetResolvedDataSource(this.DataSource, this.DataMember);

    in OnDataBinding() with:

    IEnumerable dataSource = null;

    dataSource = GetResolvedDataSource(this.DataSource, this.DataMember);

    // Code amended to allow use of DataSourceID with a SqlDataSource control

    if (dataSource == null && this.DataSourceID != null)
    {
    SqlDataSource sqlds = (SqlDataSource)this.FindControl(this.DataSourceID);

    dataSource = sqlds.Select(DataSourceSelectArguments.Empty);

    }

    It now works using either Datasource or DataSourceID.

  • Hi its fine and its very useful for lot of peoples and i have one issue i want to order by datafield values

  • Can the code handle binding data using EntityDataSource.

  • thanks alot, this saved me in the current project i am working on may GOD almighty bless you

  • Obrigado, gracias, thank you from Brazil.

  • Can we add this control to each of the row of a GridView?

  • @Sudhanshu Yes, you can

  • ddlPropertyType.DataTextField = "SubPropertyTypeName";
    ddlPropertyType.DataValueField = "SubPropertyTypeID";
    ddlPropertyType.DataGroupField = "PropertyTypeName";
    ddlPropertyType.DataBind();

    ListItem selectAnItem = new ListItem("Select...", "-1");
    selectAnItem.Attributes.Add("DataGroupField", "");
    selectAnItem.Attributes.Add("disabled", "true");
    selectAnItem.Attributes.Add("JustAnyRandomAttribute", "JustToCheck");
    ddlPropertyType.Items.Insert(0, selectAnItem);

    I tried this but the attributes are not getting added into HTML. I wonder why ?

  • Thank you so much, It helps me a lot.

  • Hi there. I don't get it working, because ASP.NET will never call the RenderContents method in this simple example:

    <uc:GroupDropDownList ID="gddl" runat="server" DataValueField="V" DataGroupField="G" DataTextField="T" DataSource='<%# new[] { new { V = "val1", G = "g1", T = "t1" }, new { V = "val2", G = "g2", T = "t2" }, new { V = "val3", G = "g3", T = "t3" } } %>' />

    On code behind I do

    this.DataBind();

    Why it's not working?

  • @AlphWav By the time I was writing that code, I supported only DataTables. You may have to debug the code on methods `GetResolvedDataSource` and `OnDataBinding`

  • Hi, I have try using your method and its work, but I got problems with data field having the same word:-

    Let say I have two columns 'Asset Type' and 'Asset Group' . Asset Type have 2 rows data which 'printer' & 'printer toner'. For printer toner have asset group 'one time' and printer asset group is null.

    My problem is in dropdownlist, 'printer' is categorized under 'One Time' asset group.

  • Hi ,I have been trying to raise index change events it's not happening in this dropdown. can you say a way to work that out. I googled and applied some technique but no result. i need to make that work.

  • This is amazing, thank you so much!

    Just wondering how would I be able to add an empty entry that is a default selection that is not within an empty group?

    Thanks

  • How can I do to use the class directly in my solution and not as an external dll?

  • Wow. Nice.
    I performed a variation on this procedure which worked, but owe most of the success to this 'GroupDropDownList' cs.
    So... assuming that the 'GroupDropDownList' dll is created and referenced as explained above.

    Instead of using a DataBind, I add entries to the GroupDropDownList via a function/method below... which can be expanded to adding a List instead of a single title/value... left as an exercise for the user.
    private void _addSelectItem(GroupDropDownList.GroupDropDownList list, string title, string value, string group = null)
    {
    ListItem item = new ListItem(title, value);
    if (!String.IsNullOrEmpty(group))
    {
    item.Attributes["DataGroupField"] = group;
    }
    else
    {
    item.Attributes["DataGroupField"] = "";
    }
    list.Items.Add(item);
    }
    It can be improved obviously... the default of null for the 4th parameter is silly in this case. But, that's the main change in the variation working for me. The main challenge is that item.Attributes["DataGroupField"] MUST be defined. If skipped for any entry in the GroupDropDownList, the execution fails. (Builds fine, but fails when trying to run on the page with the GroupDropDownList.)

    Thank you so much for posting this blog, and keeping it available for 8 years.
    Best wishes.

Add a Comment

As it will appear on the website

Not displayed

Your website